Java多线程系列--“JUC锁”09之 CountDownLatch原理和示例
概要
前面对"独占锁"和"共享锁"有了个大致的了解;和ReadWriteLock.ReadLock一样,CountDownLatch的本质也是一个"共享锁"。
CountDownLatch简介
CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
CountDownLatch和CyclicBarrier的区别
(01) CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待。
(02) CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier。
关于CyclicBarrier的原理,后面一章再来学习。
示例
CountDownLatch我理解是一个有多道锁的门闩,CountDownLatch在创建的时候就指定好有多少道锁链了。具体是这样的,假如有个门闩 CountDownLatch latch = new CountDownLatch(5),则这个门闩上面有5道锁链,当latch == 0 的时候说明门闩上5道锁都被解开了,这时候门闩打开了。
当线程调用latch.await方法的时候,会去检查latch内锁的数量是否等于0,也就是门闩是否打开了。
如果等于0说明门闩打开了,则不会被阻塞调用线程,直接运行后面的逻辑;如果 latch > 0 比如latch == 2 说明门闩上面还有2道锁,没打开,这个时候调用latch.await方法的线程就会被阻塞。
每次调用latch.countDown方法的时候,就会去掉一道锁,所以上面latch为5的时候需要调用5次countDown方法才能去掉门闩上所有的锁,让门闩打开。
示例说明:
public class CountDownLatchDemo { // 创建一个有2道锁的门闩 public static CountDownLatch latch = new CountDownLatch(2); // 等待线程,等待门闩打开 public static class WaitLatch extends Thread { @Override public void run() { try { // 等待门闩打开 System.out.println(Thread.currentThread().getName() + "被门闩卡住了"); latch.await(); // 门闩打开的时候打印一下信息 System.out.println("门闩打开啦," + Thread.currentThread().getName() + "通过啦"); } catch (InterruptedException e) { e.printStackTrace(); } } } // 调用countDown减少门闩锁的线程 public static class DownThread extends Thread { @Override public void run() { try { // 等3秒再去减少,让上面的WaitLatch线程先等着 Thread.sleep(3000); // 减少门闩锁 latch.countDown(); System.out.println("释放门闩锁"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { // 两个等待latch打开的线程 WaitLatch wait1 = new WaitLatch(); WaitLatch wait2 = new WaitLatch(); // 两个去减少latch的线程 DownThread down1 = new DownThread(); DownThread down2 = new DownThread(); wait1.start(); wait2.start(); down1.start(); down2.start(); // 等待wait1、2,down1、down2线程运行结束之后,main线程再继续执行 wait1.join(); wait2.join(); down1.join(); down2.join(); System.out.println("运行结束"); } }
上面的代码流程可以画一个图出来:
最开始的时候wait1、wait2线程调用门闩的await方法,这个时候由于门闩上面还有2道锁,所以wait1、wait2被门闩卡住了,进入等待队列,阻塞等待门闩打开。
然后down1、down2线程分别调用countDown方法各自去掉门闩的一道锁,同时检查如果门闩上没锁了,则唤醒之前被门闩卡住的线程,让他们继续运行。
CountDownLatch的原理大致上就是这样子的,但是你知道它底层源码是怎么实现的吗?
那我就带你来分析分析CountDownLatch的底层源码
02 CountDownLatch底层源码分析
CountDownLatch有一个内部类Sync,继承自AQS,重写了AQS的tryAcquireShared、tryReleaseShared方法,是一个共享锁。而CountDownLatch只是基于内部的Sync做了一层薄薄的封装而已。
那Sync实现AQS的tryAcquireShared、tryReleaseShared的逻辑是什么呢?CountDownLatch又是基于Sync之上怎么封装的?
这个我们马上就来说,我们先来一个一个看。
CountDownLatch构造方法
public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); }
上面的构造方法,内部其实就是创建一个Sync同步器,同时指定Sync内部的资源state == count,这里的state其实就是门闩上锁的数量。
await方法
public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); }
内部直接调用AQS的acquireSharedInterruptibly方法,我们继续追踪:
public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); // AQS这里调用子类的tryAcquireShared方法 // 如果返回结果大于0,继续执行业务代码 // 如果返回结果小于0,则调用doAcquireSharedInterruptibly进入AQS等待队列阻塞等待 if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }
看到这里,其实就清晰了,进入到AQS的acquireSharedInterruptibly方法获取一个锁,之前我们讲过AQS的acquireShared方法,跟这里是一样的,是一个模板方法。
首先第一个调用的就是子类Sync的tryAcquireShard方法去获取资源。
如果获取成功就返回了,继续执行业务代码。
如果获取失败了就调用doAcquireShardInterruptibly方法进入AQS等待队列进行等待,这里的doAcquireSharedInterruptibly的内部逻辑跟我们之前分析的doAcquireShared是一样的。
说到这里,就只剩下Sync的tryAcquireShared方法逻辑了,我们继续分析:
Sync的tryAcquireShared方法
protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; }
这里非常的简单,就是state == 0的时候返回1,其它情况均返回-1。
那这么说来,搞来搞去原来调用CountDownLatch的await方法是不是会被阻塞其实还是看Sync的tryAcquireShared方法是否返回1,也就是state 是否等于0咯?
没错,上面你不是说CountDownLatch是门闩嘛,其实state == 0就代表门闩上的锁都去掉了,所以门闩就打开了。
针对上面的await方法内部的逻辑和流程,我再给你画个图总结一下:
上面CountDownLatch的await方法内部的大致流程图就是这样了,你看明白了没?
大致流程是这样的吧:
调用CountDownLatch的await方法其实进入的是AQS的内部acquireSharedInterruptibly方法,这个是AQS内部定义的模板流程方法。
首先就是调用子类Sync的tryAcquireShared方法,也就是实际判断门闩是否打开,当state == 0 表示门闩上没锁了,打开。
当state != 0 表示门闩上还有锁,这个时候就需要进入AQS的等待队列进行等待咯,等待门闩打开后将线程唤醒。
这里CountDownLacth的await方法内部的源码和流程,就讲到这里咯。我们接下来继续,讲解CountDownLatch的countDown方法:
CountDownLatch的countDown方法
public void countDown() { sync.releaseShared(1); }
这里的countDown方法直接就是调用AQS的releaseShared(1)方法,继续进入releaseShared方法
public final boolean releaseShared(int arg) { // 调用子类的tryReleaseShared方法,释放锁 if (tryReleaseShared(arg)) { // 如果锁完全释放了,就唤醒等待队列中沉睡的线程 doReleaseShared(); return true; } return false; }
这里就是进入AQS释放共享锁的模板流程了:
首先就是调用子类的tryReleaseShared方法释放锁,如果完全释放了,也就是state == 0 的时候,就调用AQS的doReleaseShared方法唤醒等待队列中等待的线程。
我们看看子类Sync的tryReleaseShared的逻辑
Sync的tryReleaseShared
protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c = getState(); // 如果state == 0,也就是锁的数量等于0,表示门闩打开了 if (c == 0) return false; // 这里就是将state - 1,也就是将门闩上锁的数量减少一道 int nextc = c-1; // CAS操作重新设置锁的数量 if (compareAndSetState(c, nextc)) return nextc == 0; } }
上面的代码非常简单,也就是将state的值减少1而已,表示将锁的数量减少一道。countDown内部的核心逻辑其实就是将state的数量减少1,也就是锁的数量减少1,当state == 0的时候,表示门闩已经打开,就可以调用doReleaseShared方法将AQS等待队列的线程唤醒了,表示:嘿,兄弟们,别睡了,门闩打开了。再加上doReleaseShared唤醒等待队列中的线程的源码,之前讲解AQS的共享锁机制的时候已经深入分析过了,所以这里我完全没有问题。
从整体上画一个CountDownLatch的await方法、countDown方法的整体流程图:
上面的这个图就是整体的CountDownLatch的await和countDown整体的流程以及交互的机制了,这下子CountDownLatch没问题了吧?
其实大概就是这样,最开始CountDownLatch latch = new CountDownLatch(2)的时候就是设置latch门闩上有2道锁。
然后线程A调用latch.await的时候发现上面还有锁,于是进入AQS等待队列睡觉去了。
线程B、C调用countDown方法,各自将锁减少一道,发现锁已经完全解开了,于是就是就唤醒了AQS中由于调用await方法陷入等待的线程。
这玩意,感觉就是一个计数开关一样,当门闩上锁为0的时候,开关打开,其它时候关闭,就是这么简单;只不过这东西是整合了AQS,利用了AQS的等待队列阻塞等待,以及唤醒机制而已。
CountDownLatch函数列表
CountDownLatch(int count) 构造一个用给定计数初始化的 CountDownLatch。 // 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。 void await() // 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。 boolean await(long timeout, TimeUnit unit) // 递减锁存器的计数,如果计数到达零,则释放所有等待的线程。 void countDown() // 返回当前计数。 long getCount() // 返回标识此锁存器及其状态的字符串。 String toString()
CountDownLatch数据结构
CountDownLatch的UML类图如下:
CountDownLatch的数据结构很简单,它是通过"共享锁"实现的。它包含了sync对象,sync是Sync类型。Sync是实例类,它继承于AQS。
CountDownLatch的使用示例
下面通过CountDownLatch实现:"主线程"等待"5个子线程"全部都完成"指定的工作(休眠1000ms)"之后,再继续运行。
1 import java.util.concurrent.CountDownLatch; 2 import java.util.concurrent.CyclicBarrier; 3 4 public class CountDownLatchTest1 { 5 6 private static int LATCH_SIZE = 5; 7 private static CountDownLatch doneSignal; 8 public static void main(String[] args) { 9 10 try { 11 doneSignal = new CountDownLatch(LATCH_SIZE); 12 13 // 新建5个任务 14 for(int i=0; i<LATCH_SIZE; i++) 15 new InnerThread().start(); 16 17 System.out.println("main await begin."); 18 // "主线程"等待线程池中5个任务的完成 19 doneSignal.await(); 20 21 System.out.println("main await finished."); 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 } 26 27 static class InnerThread extends Thread{ 28 public void run() { 29 try { 30 Thread.sleep(1000); 31 System.out.println(Thread.currentThread().getName() + " sleep 1000ms."); 32 // 将CountDownLatch的数值减1 33 doneSignal.countDown(); 34 } catch (InterruptedException e) { 35 e.printStackTrace(); 36 } 37 } 38 } 39 }
运行结果:
main await begin. Thread-0 sleep 1000ms. Thread-2 sleep 1000ms. Thread-1 sleep 1000ms. Thread-4 sleep 1000ms. Thread-3 sleep 1000ms. main await finished.
结果说明:主线程通过doneSignal.await()等待其它线程将doneSignal递减至0。其它的5个InnerThread线程,每一个都通过doneSignal.countDown()将doneSignal的值减1;当doneSignal为0时,main被唤醒后继续执行。